package com.softaria.spkiller.mojo;


import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;

import com.softaria.spkiller.analyzer.dependency.ProhibitedDependencyReporter;
import com.softaria.spkiller.analyzer.hierarchy.BrokenHierarchyReporter;
import com.softaria.spkiller.analyzer.hierarchy.Hierarchy;
import com.softaria.spkiller.analyzer.hierarchy.enclosed.EnclosedHierarchyChecker;
import com.softaria.spkiller.analyzer.hierarchy.enclosed.SheepMarker;
import com.softaria.spkiller.dependencies.ClassycleDependencyFinder;
import com.softaria.spkiller.dependencies.DependencyFinder;
import com.softaria.spkiller.dependencies.DependencyGraph;
import com.softaria.spkiller.jaxb.configuration.Check;
import com.softaria.spkiller.jaxb.configuration.Classification;
import com.softaria.spkiller.jaxb.configuration.Configuration;
import com.softaria.spkiller.jaxb.configuration.DependencyCheck;
import com.softaria.spkiller.jaxb.configuration.DependencyCheck.AllowedDependency;
import com.softaria.spkiller.jaxb.configuration.EnclosedHierarchiesCheck;
import com.softaria.spkiller.jaxb.configuration.HierarchyCheck;
import com.softaria.spkiller.layers.LayerIdentifier;
import com.softaria.spkiller.layers.LayerIdentifierImpl;
import com.softaria.spkiller.layers.classificator.Classificator;
import com.softaria.spkiller.layers.classificator.LayerManager;
import com.softaria.spkiller.layers.classificator.impl.IOProblemCollector;
import com.softaria.spkiller.layers.classificator.impl.SoftariaClassificator;
import com.softaria.spkiller.metadata.ClassIdentifier;
import com.softaria.spkiller.metadata.extractor.AsmMetaDataExtractor;
import com.softaria.spkiller.metadata.extractor.InputStreamProvider;
import com.softaria.spkiller.metadata.extractor.InputStreamProviderImpl;
import com.softaria.spkiller.metadata.extractor.MetaDataExtractor;

/**
 * @goal check
 * 
 * @phase process-classes
 */
@SuppressWarnings("restriction")
public class SpaghettiKiller extends AbstractMojo {

	private static final String CLASSES = "classes";

	private static final String JAXB_CONFIGURATION_PACKAGE = "com.softaria.spkiller.jaxb.configuration";

	/**
	 * The list of resources we want to transfer.
	 * 
	 * @parameter default-value="${project.resources}"
	 * @required
	 * @readonly
	 */
	private List<Resource> resources;

	/**
	 * @parameter expression="${project.artifacts}"
	 * @required
	 */
	private Set<Artifact> projectDependencies;

	/**
	 * Location of the file.
	 * 
	 * @parameter expression="${project.build.directory}"
	 * @required
	 */
	private File outputDirectory;
	
	
	/**
	 * 
	 * @parameter expression="${java.home}"
	 * @required
	 */
	private File javaHome;
	

	/**
	 * 
	 * @parameter
	 * @required
	 */
	private String configurationFilePath;// = "spaghetti-killer.xml";


	/**
	 * 
	 * @parameter expression="${check.quiet}"
	 */
	private boolean quiet = false;


	
	private Configuration config = null;

	public void execute() throws MojoExecutionException {

		log("has been started");
		
		for (Resource r : resources) {
			File confFile = new File(r.getDirectory(), configurationFilePath);
			
			log("trying to get configuration file from: "+confFile.getAbsolutePath());
			
			if (confFile.exists()) {
				try {
					log("configuration file has been found here: "+confFile.getAbsolutePath());
					loadConfiguration(confFile);
					log("configuration has been loaded");
					break;
					
				} catch (Exception ex) {
					ex.printStackTrace();
					throw new MojoExecutionException(
							"Can't load configuration form file "
									+ confFile.getAbsolutePath(), ex);
				}
			}
		}
		
		if(config == null) {
			System.err.println("SPAGHETTI_KILLER: No configuration file exists for: " + configurationFilePath+" in any resource folder");
			System.err.println("SPAGHETTI_KILLER: exiting");
			return;
		}

		log("Retrieving dependencies");
		
		// Handle dependencies
		List<String> pathesToLibraries = new ArrayList<String>();
		for (Iterator<Artifact> i = projectDependencies.iterator(); i.hasNext();) {
			Artifact pluginArtifact = i.next();

			pathesToLibraries.add(pluginArtifact.getFile().getAbsolutePath());
			
			log("dependency: "+pluginArtifact.getFile().getAbsolutePath());
		}
		
		//Adding pathes to JAVA standard libraries
		log("Looking for standard JAVA runtime environment...");
		addJavaStandardLibraries(javaHome,pathesToLibraries);

		log("Retrieving classes of the current project");
		// Handle own classes
		File f = outputDirectory;
		if (!f.exists()) {
			throw new MojoExecutionException("No target directory exists for "
					+ f);
		}
		File classesFolder = new File(f, CLASSES);
		if (!classesFolder.exists()) {
			throw new MojoExecutionException("No classes directory exists for "
					+ classesFolder);
		}

		// Prepare a whole set where classes can be located (dependencies + own)
		String[] pathesToFoldersOrJars = new String[pathesToLibraries.size() + 1];

		for (int i = 0; i < pathesToLibraries.size(); i++) {
			pathesToFoldersOrJars[i] = (String) pathesToLibraries.get(i);
		}
		pathesToFoldersOrJars[pathesToFoldersOrJars.length - 1] = classesFolder
				.getAbsolutePath();

		log("Own classes have been retrieved");
		
		try {
			log("Starting: Preparing input stream provider...");
			// Creating file input stream provider
			InputStreamProvider isProvider = new InputStreamProviderImpl(
					pathesToFoldersOrJars);
			log("Starting: Preparing meta data extractor...");
			// Creating file metadata extractor
			final MetaDataExtractor extractor = new AsmMetaDataExtractor(
					isProvider);

			log("Starting: Instantiating I/O problem collector...");
			MojoIoProblemCollector ioProblemCollector = new MojoIoProblemCollector();

			log("Starting: Classificator...");
			Classificator classificator = prepareClassificator(extractor,
					ioProblemCollector);

			log("Starting: Dependency finder...");
			DependencyFinder depFinder = new ClassycleDependencyFinder();

			log("Calculating dependency graph...");
			DependencyGraph depGraph = depFinder.findDependencies(
					new String[] { classesFolder.getAbsolutePath() }, null,
					true);

			log("Initializing layer manager...");
			LayerManager lm = LayerManager.createLayerManager(
					depGraph.getAllClasses(), classificator);
			
			reportLayers(lm);

			log("RUNNING CHECKS");
			processChecks(depGraph, lm, classificator);

			ioProblemCollector.check();

		} catch (IOException e) {
			e.printStackTrace();
			throw new MojoExecutionException("I/O error occured ", e);
		}

	}


	private void reportLayers(LayerManager lm) {
		Collection<LayerIdentifier> layers = lm.getAllLayers();
		
		log("Number of layers is "+layers.size());

		int total = 0;
		
		for(LayerIdentifier layerIdentifier:layers) {
			int classesInLayer = lm.getLayerClasses(layerIdentifier).size();
			total += classesInLayer;
			log("Layer "+layerIdentifier.getName()+". Number of classes: "+classesInLayer);
		}
		
		log("total number of layered classes is "+total);
		
		int allClassCount = lm.getAllClasses().size();
		
		log("total number of all classes is "+allClassCount);
		
		int percent = 100* ((int)((double)total)/allClassCount);
		
		log(percent+"% of classes are layered");
		
	}


	private void log(String s) {
		if(!quiet) System.out.println("SPAGHETTI-KILLER "+s);
	}

	
	private void addJavaStandardLibraries(File javaHome,
			List<String> pathesToLibraries) {
		
		File path = null;
		
		File pathToLibInJDK = new File(javaHome,"jre/lib");
		
		if(pathToLibInJDK.exists()) {
			log("Java runtime environment is JDK. It's lib folder has been found at "+pathToLibInJDK.getAbsolutePath());
			//it is JDK
			path = pathToLibInJDK;
		}
		else {
			//it is JRE
			path = new File(javaHome,"lib");
			log("Java runtime environment is JRE. It's lib folder has been found at "+path.getAbsolutePath());
		}
		
		if(path.exists()&&path.isDirectory()) {
			File pathes[] = path.listFiles(new FileFilter() {
				public boolean accept(File pathname) {
					return pathname.getName().toLowerCase().endsWith(".jar");
				}
			});
			
			log("adding standard JAVA libraries");
			for(File p:pathes) {
				pathesToLibraries.add(p.getAbsolutePath());
				log("JAVA lib: "+p.getAbsolutePath());
			}
		}
		else {
			log("ERROR: no JRE lib folder found at: "+path.getAbsolutePath());
		}
		
	}


	private void processChecks(DependencyGraph depGraph, LayerManager lm,
			Classificator classificator) throws MojoExecutionException {
		
		for( JAXBElement<? extends Check> checkElement :config.getCheck()) {
			Check check = checkElement.getValue();
			
			if(check instanceof DependencyCheck) {
				log("CHECKING DEPENDENCIES");
				checkDependency((DependencyCheck)check,depGraph,lm,classificator);
			}
			
			if(check instanceof HierarchyCheck) {
				log("CHECKING HIERARCHY");
				checkHierarchy(((HierarchyCheck)check).getLayer(),depGraph,lm,classificator);
			}
			
			if(check instanceof EnclosedHierarchiesCheck) {
				log("CHECKING ENCLOSED HIERARCHY");
				checkEnclosedHierarchies((EnclosedHierarchiesCheck)check,depGraph,lm,classificator);
			}
			
		}
		
		
	}

	

	@SuppressWarnings("unused")
	private void checkEnclosedHierarchies(EnclosedHierarchiesCheck check,
			DependencyGraph depGraph, LayerManager lm,
			Classificator classificator) throws MojoExecutionException {
		
		log("master layer="+check.getMasterLayer()+", slave layer = "+check.getSlaveLayer());
		
		LayerIdentifier shepherdLayerID = new LayerIdentifierImpl(check.getMasterLayer());
		
		Collection<ClassIdentifier> shepherdsClasses = lm.getLayerClasses(shepherdLayerID);
		
		Collection<Hierarchy<Void>> shepherdHierarchies = checkHierarchy(check.getMasterLayer(), depGraph, lm, classificator);
		
		LayerIdentifier sheepsLayerID = new LayerIdentifierImpl(check.getSlaveLayer());
		
		Collection<ClassIdentifier> sheepsClasses = lm.getLayerClasses(sheepsLayerID);
		
		Collection<Hierarchy<SheepMarker>> sheepsHierarchies = checkHierarchy(check.getSlaveLayer(), depGraph, lm, classificator);
		
		EnclosedHierarchyChecker reporter = new EnclosedHierarchyChecker(shepherdHierarchies, sheepsHierarchies, classificator, shepherdLayerID, sheepsLayerID);
		
		MojoEnclosedHierarchyProblemReporter problemReporter = new MojoEnclosedHierarchyProblemReporter();

		reporter.checkEnclosed(problemReporter);
		
		problemReporter.check();
		
		log("check OK");
		
	}

	private <T> Collection<Hierarchy<T>> checkHierarchy(String layerName,
			DependencyGraph depGraph, LayerManager lm,
			Classificator classificator) throws MojoExecutionException {
		
		log("Checking hierarchycal layer named "+layerName);

		BrokenHierarchyReporter<T> brokenHierarchyReporter = new BrokenHierarchyReporter<T>();
					
		LayerIdentifier layerID = new LayerIdentifierImpl(layerName);
					
		Collection<ClassIdentifier> classes = lm.getLayerClasses(layerID);

		MojoHierarchyProblemCollector brokenHierarchyCollector = new MojoHierarchyProblemCollector(layerID);
					
		Collection<Hierarchy<T>> hierarchies = brokenHierarchyReporter.checkHierarchy(depGraph, classes, brokenHierarchyCollector);
				
		brokenHierarchyCollector.check();
		
		log("check OK");
		
		return hierarchies;
		
	}

	private void checkDependency(DependencyCheck check,
			DependencyGraph depGraph, LayerManager lm,
			Classificator classificator) throws MojoExecutionException {
		
		log("checking dependencies from "+check.getLayer());
		
		ProhibitedDependencyReporter dependencyChecker = new ProhibitedDependencyReporter();

		MojoDependencyProblemCollector dependencyProblemCollector = new MojoDependencyProblemCollector();

		LayerIdentifier layerID = new LayerIdentifierImpl(check.getLayer());
		
		Collection<ClassIdentifier> classesToCheck = lm.getLayerClasses(layerID);
		
		Collection<LayerIdentifierImpl> layersAllowed = new ArrayList<LayerIdentifierImpl>();
		
		
		for(AllowedDependency allowedDependency: check.getAllowedDependency()) {
			layersAllowed.add(new LayerIdentifierImpl(allowedDependency.getTo()));
		}
		
		dependencyChecker.reportProblem(depGraph, classificator, classesToCheck, layersAllowed, dependencyProblemCollector);
		
		dependencyProblemCollector.check();
		
		log("check OK");
	}

	@SuppressWarnings({ "unchecked" })
	private void loadConfiguration(File confFile) throws IOException,
			JAXBException {

		FileInputStream is = new FileInputStream(confFile);

		try {

			JAXBContext jaxbContext = JAXBContext
					.newInstance(JAXB_CONFIGURATION_PACKAGE);

			JAXBElement<Configuration> conf = (JAXBElement<Configuration>) jaxbContext
					.createUnmarshaller().unmarshal(is);

			config = conf.getValue();

		} finally {
			is.close();
		}

	}

	private Classificator prepareClassificator(
			final MetaDataExtractor extractor,
			IOProblemCollector ioProblemCollector) {

		SoftariaClassificator classificator = new SoftariaClassificator(extractor,
				ioProblemCollector);

		Classification classification = config.getClassification();

		classificator.loadConfiguration(classification.getRule());

		return classificator;

	}

}
